package br.jm.i18n;

import static org.objectweb.asm.Opcodes.AASTORE;
import static org.objectweb.asm.Opcodes.ACC_FINAL;
import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
import static org.objectweb.asm.Opcodes.ACC_VARARGS;
import static org.objectweb.asm.Opcodes.ALOAD;
import static org.objectweb.asm.Opcodes.ANEWARRAY;
import static org.objectweb.asm.Opcodes.ARETURN;
import static org.objectweb.asm.Opcodes.ASM4;
import static org.objectweb.asm.Opcodes.BIPUSH;
import static org.objectweb.asm.Opcodes.DLOAD;
import static org.objectweb.asm.Opcodes.DUP;
import static org.objectweb.asm.Opcodes.FLOAD;
import static org.objectweb.asm.Opcodes.ICONST_0;
import static org.objectweb.asm.Opcodes.ICONST_1;
import static org.objectweb.asm.Opcodes.ICONST_2;
import static org.objectweb.asm.Opcodes.ICONST_3;
import static org.objectweb.asm.Opcodes.ICONST_4;
import static org.objectweb.asm.Opcodes.ILOAD;
import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
import static org.objectweb.asm.Opcodes.INVOKESTATIC;
import static org.objectweb.asm.Opcodes.LLOAD;
import static org.objectweb.asm.Opcodes.RETURN;

import java.lang.invoke.MethodType;
import java.text.MessageFormat;
import java.util.Locale;

import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;

public class I18NGeneratorTypeSafe extends ClassVisitor {

	class KeyCollector extends MethodVisitor {

		final String name;
		final Type[] argTypes;
		String key;

		public KeyCollector(final MethodVisitor mv, final String name, final Type[] argTypes) {
			super(ASM4, mv);
			this.name = name;
			this.argTypes = argTypes;
		}

		void intInsn(final int i) {
			switch (i) {
			case 0:
				visitInsn(ICONST_0);
				break;
			case 1:
				visitInsn(ICONST_1);
				break;
			case 2:
				visitInsn(ICONST_2);
				break;
			case 3:
				visitInsn(ICONST_3);
				break;
			case 4:
				visitInsn(ICONST_4);
				break;
			default:
				if (i < 256) {
					visitIntInsn(BIPUSH, i);
				}
			}
		}

		private String keyByConvention() {
			return originalName.replace('/', '.') + "." + name;
		}

		private void loadAndBox(final Type type, final short stackIndex) {

			switch (type.getSort()) {
			case Type.BOOLEAN:
				visitVarInsn(ILOAD, stackIndex);
				visitMethodInsn(INVOKESTATIC, "java/lang/Boolean", "valueOf", "(Z)Ljava/lang/Boolean;");
				break;

			case Type.BYTE:
				visitVarInsn(ILOAD, stackIndex);
				visitMethodInsn(INVOKESTATIC, "java/lang/Byte", "valueOf", "(B)Ljava/lang/Byte;");
				break;

			case Type.CHAR:
				visitVarInsn(ILOAD, stackIndex);
				visitMethodInsn(INVOKESTATIC, "java/lang/Character", "valueOf", "(C)Ljava/lang/Character;");
				break;

			case Type.SHORT:
				visitVarInsn(ILOAD, stackIndex);
				visitMethodInsn(INVOKESTATIC, "java/lang/Short", "valueOf", "(S)Ljava/lang/Short;");
				break;

			case Type.INT:
				visitVarInsn(ILOAD, stackIndex);
				visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;");
				break;

			case Type.FLOAT:
				visitVarInsn(FLOAD, stackIndex);
				visitMethodInsn(INVOKESTATIC, "java/lang/Float", "valueOf", "(F)Ljava/lang/Float;");
				break;

			case Type.LONG:
				visitVarInsn(LLOAD, stackIndex);
				visitMethodInsn(INVOKESTATIC, "java/lang/Long", "valueOf", "(J)Ljava/lang/Long;");
				break;

			case Type.DOUBLE:
				visitVarInsn(DLOAD, stackIndex);
				visitMethodInsn(INVOKESTATIC, "java/lang/Double", "valueOf", "(D)Ljava/lang/Double;");
				break;
			default:
				visitVarInsn(ALOAD, stackIndex);
			}
		}

		@Override
		public AnnotationVisitor visitAnnotation(final String desc, final boolean visible) {
			if (I18N_DESC.equals(desc)) {
				return new AnnotationVisitor(ASM4) {

					@Override
					public void visit(final String name, final Object value) {
						key = (String) value;
					}

				};
			}

			return null;
		}

		@Override
		public void visitCode() {
			if ((key == null) || key.isEmpty()) {
				key = keyByConvention();
			}

			final String value = ms.getMessage(key, loc);

			visitLdcInsn(value);

			if (argTypes == null) {
				visitVarInsn(ALOAD, 1);
				visitMethodInsn(INVOKESTATIC, Type.getInternalName(MessageFormat.class), "format", MF_DESC);
			} else if (argTypes.length > 0) {
				final short[] indexes = calculateIndexes(argTypes);

				intInsn(indexes.length);
				visitTypeInsn(ANEWARRAY, "java/lang/Object");

				for (int i = 0; i < indexes.length; i++) {
					visitInsn(DUP);
					intInsn(i);// array index
					final short stackIndex = indexes[i];
					loadAndBox(argTypes[i], stackIndex);
					visitInsn(AASTORE);
				}

				visitMethodInsn(INVOKESTATIC, Type.getInternalName(MessageFormat.class), "format", MF_DESC);
			}

			visitInsn(ARETURN);
			visitMaxs(0, 0);
		}

		@Override
		public void visitEnd() {
			visitCode();

			super.visitEnd();
		}
	}

	static final String I18N_DESC = Type.getDescriptor(I18N.class);

	static final String NO_ARGS_DESC = MethodType.methodType(String.class).toMethodDescriptorString();

	static final String WITH_ARGS_DESC = MethodType.methodType(String.class, Object[].class).toMethodDescriptorString();

	static final String MF_DESC = MethodType.methodType(String.class, String.class, Object[].class).toMethodDescriptorString();

	static short[] calculateIndexes(final Type[] ptypes) {
		final short[] s = new short[ptypes.length];

		s[0] = 1;

		for (int i = 1; i < s.length; i++) {
			final Type prevType = ptypes[i - 1];
			final short incr = (short) (((prevType == Type.LONG_TYPE) || (prevType == Type.DOUBLE_TYPE)) ? 2 : 1);
			s[i] = (short) (s[i - 1] + incr);
		}

		return s;
	}

	final MessageProvider ms;
	final Locale loc;
	String targetName;
	String originalName;

	public I18NGeneratorTypeSafe(final ClassVisitor cv, final MessageProvider ms, final Locale loc) {
		super(ASM4, cv);
		this.ms = ms;
		this.loc = loc;
	}

	private void emitCtor() {
		final MethodVisitor mv = super.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
		mv.visitCode();
		mv.visitVarInsn(ALOAD, 0);
		mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V");
		mv.visitInsn(RETURN);
		mv.visitMaxs(0, 0);
		mv.visitEnd();
	}

	@Override
	public void visit(final int version, final int access, final String name, final String signature, final String superName, final String[] interfaces) {
		originalName = name;
		targetName = name + "@" + loc;
		super.visit(version, ACC_FINAL + ACC_PUBLIC, targetName, signature, superName, new String[]{ name });
	}

	@Override
	public void visitEnd() {

		emitCtor();

		super.visitEnd();
	}

	@Override
	public MethodVisitor visitMethod(final int access, final String name, final String desc, final String signature, final String[] exceptions) {
		final MethodVisitor mv = super.visitMethod(ACC_FINAL + ACC_PUBLIC + (access & ACC_VARARGS), name, desc, signature, exceptions);

		final boolean isArrayParam = WITH_ARGS_DESC.equals(desc);

		Type[] argTypes = null;
		if (!isArrayParam) {
			if (!NO_ARGS_DESC.equals(desc)) {
				if (!Type.getReturnType(desc).equals(Type.getType(String.class))) {

				} else {
					argTypes = Type.getArgumentTypes(desc);
				}
			} else {
				argTypes = new Type[0];
			}
		}

		return new KeyCollector(mv, name, argTypes);
	}
}